package com.cw.lang.common.framework.feign;

import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import javax.annotation.Nonnull;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;

import com.cw.base.common.model.dto.response.CommonRespDto;
import com.cw.base.common.model.enums.EnumRpcResult;
import com.cw.lang.common.constants.HttpStatusConstant;
import com.cw.lang.common.enums.EnumResponseType;
import com.cw.lang.common.exception.SystemException;
import com.cw.lang.common.exception.TimeoutException;
import com.cw.lang.common.framework.config.FrameworkRpcProperties;
import com.cw.lang.common.framework.enums.EnumRpcError;
import com.cw.lang.common.framework.okhttp.LocalInterceptor;
import com.cw.lang.common.framework.utils.CommonResponseUtil;
import com.cw.lang.common.framework.utils.RpcAppParamUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Range;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

/**
 * @author chenw
 * @version $$Id: RpcTimeoutFeignInterceptor.java, v 0.1 2019/2/23 2:19 PM chenw Exp $$
 */
@Setter
@Slf4j
public class RpcExceptionFeignInterceptor implements LocalInterceptor {

    private static final Integer ORDERED = Ordered.HIGHEST_PRECEDENCE + 2000;

    private ObjectMapper objectMapper;

    private FrameworkRpcProperties.RpcProperties rpcProperties;

    @Override
    public Response intercept(@Nonnull Interceptor.Chain chain) throws IOException {
        Request request = chain.request();
        try {
            Response response = chain.proceed(request);
            if (Range.closed(HttpStatusConstant.SUCCESS_MIN, HttpStatusConstant.SUCCESS_MAX)
                .contains(response.code())) {
                return processInCommonResponse(response);
            } else {
                return processInFeignResponse(response);
            }
        } catch (Exception e) {
            return processException(request, e);
        }
    }

    /**
     * 如果抛异常，则对异常进行封装
     *
     * @param request 请求入参
     * @param e 异常
     * @return 封装为 {@link Response}
     * @throws IOException 异常
     */
    private Response processException(Request request, Exception e) throws IOException {
        boolean rpcExceptionThrowEnabled = isRpcExceptionThrowEnabled();
        // TODO: 默认使用Protocol.HTTP_1_1 后续考虑如何从参数中取出来
        Protocol defaultProtocol = Protocol.HTTP_1_1;
        if (e instanceof SocketTimeoutException) {
            if (rpcExceptionThrowEnabled) {
                throw new TimeoutException(String.valueOf(HttpStatus.REQUEST_TIMEOUT.value()),
                    HttpStatus.REQUEST_TIMEOUT.getReasonPhrase());
            } else {
                CommonRespDto commonRespDto = generateCommonResponse(HttpStatus.REQUEST_TIMEOUT.value());
                return new Response.Builder().request(request)
                    .addHeader(HttpHeaders.CONTENT_TYPE, getContentTypeFromRequest(request))
                    .protocol(defaultProtocol)
                    .code(HttpStatus.OK.value())
                    .body(ResponseBody.create(generateMediaType(request.headers()
                        .get(HttpHeaders.CONTENT_TYPE)), objectMapper.writeValueAsBytes(commonRespDto)))
                    .message(HttpStatus.REQUEST_TIMEOUT.getReasonPhrase())
                    .build();
            }
        } else {
            if (rpcExceptionThrowEnabled) {
                throw new IOException(e);
            } else {
                // 抛异常，统一封装为500异常
                CommonRespDto commonRespDto = generateCommonResponse(HttpStatus.INTERNAL_SERVER_ERROR.value());
                return new Response.Builder().request(request)
                    .addHeader(HttpHeaders.CONTENT_TYPE, getContentTypeFromRequest(request))
                    .protocol(defaultProtocol)
                    .code(HttpStatus.OK.value())
                    .body(ResponseBody.create(generateMediaType(request.headers()
                        .get(HttpHeaders.CONTENT_TYPE)), objectMapper.writeValueAsBytes(commonRespDto)))
                    .message(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
                    .build();
            }
        }
    }

    /**
     * 获取Content-Type
     *
     * @param request 请求入参
     * @return Content-Type 默认值为application/json;charset=UTF-8
     */
    private String getContentTypeFromRequest(Request request) {
        String headerValue = request.header(HttpHeaders.CONTENT_TYPE);
        return StringUtils.isBlank(headerValue) ? org.springframework.http.MediaType.APPLICATION_JSON_UTF8_VALUE :
            headerValue;
    }

    /**
     * 处理存储在{@link CommonRespDto}中的异常
     *
     * @param response feign的响应
     * @return 重新包装的feign响应
     * @throws IOException 异常
     */
    private Response processInCommonResponse(Response response) throws IOException {
        ResponseBody body = response.body();
        if (body != null) {
            byte[] content = body.bytes();
            if (ArrayUtils.isNotEmpty(content)) {
                // TODO: 自定义解析器，仅解析对象中非data的参数，避免对象太大，解析耗时
                CommonRespDto commonRespDto = objectMapper.readValue(content, CommonRespDto.class);
                // 同步把CommonResponse塞到线程上下文中
                CommonResponseUtil.setCommonResponse(commonRespDto);
                Integer httpStatus = commonRespDto.getHttpStatus();
                // 如果是成功请求
                if (Range.closed(HttpStatusConstant.SUCCESS_MIN, HttpStatusConstant.SUCCESS_MAX)
                    .contains(httpStatus)) {
                    return response.newBuilder()
                        .body(ResponseBody.create(generateMediaType(response.headers()
                            .get(HttpHeaders.CONTENT_TYPE)), content))
                        .build();
                }
                // 如果不是成功请求
                else {
                    return processExceptionInCommonResponse(response, commonRespDto);
                }
            }
        }
        return response.newBuilder()
            .build();
    }

    /**
     * 针对于非成功的请求，做包装
     *
     * @param response response
     * @param commonRespDto 自己包装的响应
     * @return 新的Response
     * @throws JsonProcessingException 异常
     */
    private Response processExceptionInCommonResponse(Response response, CommonRespDto commonRespDto)
    throws JsonProcessingException {
        Integer httpStatus = commonRespDto.getHttpStatus();
        boolean isTimeout = isTimeout(httpStatus);
        // 抛异常处理
        if (isRpcExceptionThrowEnabled()) {
            // 是超时异常
            if (isTimeout) {
                throw new TimeoutException(EnumRpcError.RPC_TIME_OUT.getCode(),
                    EnumRpcError.RPC_TIME_OUT.getDescription());
            }
            // 不是超时异常
            else {
                // 组装为系统可识别的异常
                throw new SystemException(httpStatus, String.valueOf(httpStatus), HttpStatus.valueOf(httpStatus)
                    .getReasonPhrase());
            }
        }
        // 封装为CommonResponse处理
        else {
            CommonRespDto newCommonRespDto = generateCommonResponse(httpStatus);
            return response.newBuilder()
                .code(HttpStatus.OK.value())
                .body(ResponseBody.create(generateMediaType(response.headers()
                    .get(HttpHeaders.CONTENT_TYPE)), objectMapper.writeValueAsBytes(newCommonRespDto)))
                .build();
        }
    }

    /**
     * 处理存储在{@link Response}中的异常
     *
     * @param response feign的响应
     * @return 重新包装的feign响应
     * @throws JsonProcessingException 异常
     */
    private Response processInFeignResponse(Response response) throws JsonProcessingException {
        boolean isTimeout = isTimeout(response.code());
        if (isRpcExceptionThrowEnabled()) {
            if (isTimeout) {
                throw new TimeoutException(EnumRpcError.RPC_TIME_OUT.getCode(),
                    EnumRpcError.RPC_TIME_OUT.getDescription());
            } else {
                // 组装异常
                throw new SystemException(response.code(), String.valueOf(response.code()),
                    HttpStatus.valueOf(response.code())
                        .getReasonPhrase());
            }
        } else {
            CommonRespDto commonRespDto = generateCommonResponse(response.code());
            return response.newBuilder()
                .code(HttpStatus.OK.value())
                .body(ResponseBody.create(generateMediaType(response.headers()
                    .get(HttpHeaders.CONTENT_TYPE)), objectMapper.writeValueAsBytes(commonRespDto)))
                .build();
        }
    }

    /**
     * 是否超时异常
     *
     * @param httpsStatus response中的httpStatus
     * @return true-是超时异常，false-不是超时异常
     */
    private boolean isTimeout(Integer httpsStatus) {
        return rpcProperties.isTimeout(httpsStatus);
    }

    /**
     * 是否要把异常向上抛
     *
     * @return 异常
     */
    private boolean isRpcExceptionThrowEnabled() {
        return RpcAppParamUtil.isRpcExceptionThrowEnabled();
    }

    /**
     * 生成{@link MediaType}
     *
     * @param contentType header中的contentType
     * @return {@link MediaType}
     */
    private MediaType generateMediaType(String contentType) {
        Charset charset = StandardCharsets.UTF_8;
        return contentType != null ? MediaType.parse(contentType) : MediaType.parse(charset.displayName());
    }

    /**
     * 生成{@link CommonRespDto}对象json的byte数组
     *
     * @param httpStatus http状态
     * @return byte数组
     */
    private CommonRespDto generateCommonResponse(Integer httpStatus) {
        CommonRespDto.CommonRespDtoBuilder builder = CommonRespDto.builder()
            .httpStatus(httpStatus)
            .parentIdemSerialId(null)
            .responseType(EnumResponseType.SYS_ERR);
        boolean timeout = isTimeout(httpStatus);
        if (timeout) {
            builder.rpcResult(EnumRpcResult.TIMEOUT_ERR);
        } else {
            builder.rpcResult(EnumRpcResult.FAIL);
        }
        CommonRespDto commonRespDto = builder.build();
        // 同步把CommonResponse塞到线程上下文中
        CommonResponseUtil.setCommonResponse(commonRespDto);
        return commonRespDto;
    }

    @Override
    public int getOrder() {
        return ORDERED;
    }

}
